import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from contextlib import contextmanager
from queue import Queue, Empty
from threading import Thread, Event

import requests

from app.common.log_utils import logger
from app.common.time_utils import Datetime, trading_time
from app.common.utils import init_executor
from app.globals import cache, env
from app.macro import SendOrderCategory, SendOrderPriceType, QueryDataCategory, ACCOUNTS, SHARES, TradeCode
from libs.tdx.dll_api import logon, query_data, send_order, cancel_order
from app.mod_trade.api.tdx.utils import retry, parse_buy_or_sell, parse_cancel, get_query_parse
from app.mod_trade.model.trade_worker import TradeWorker


class Cid:
    def __init__(self, cid, state):
        self.cid = cid
        self.state = state


class CidPool:
    def __init__(self, account, cid_num=3):
        self.account = account
        self.cid_num = cid_num
        self.q = Queue()

    def logon(self):
        r = logon(self.account.trade_server,
                  self.account.port,
                  '2',
                  self.account.yyb_id,
                  self.account.account,
                  self.account.password)
        logger.info(f'{self.account.account} logon {r["success"]} {r["error"]}')
        return Cid(r['client_id'], r['success'])

    def setup(self):
        self.q.queue.clear()
        for i in range(self.cid_num):
            cid = self.logon()
            if cid.state:
                self.q.put(cid)

    def get(self):
        try:
            return self.q.get_nowait()
        except Empty:
            return self.logon()

    def put(self, cid):
        self.q.put(cid)


class TdxDllWorker(Thread, TradeWorker):
    def __init__(self, account, cid_num=3, interval=3):
        super().__init__()
        self.daemon = True
        self.finished = Event()
        self.account = account
        self.cid_pool = CidPool(account, cid_num)
        self.interval = interval
        self.action_mapping = {
            TradeCode.QUERY: self._query,
            TradeCode.BUY: self._buy,
            TradeCode.SELL: self._sell,
            TradeCode.CANCEL: self._cancel
        }
        self.executor = ThreadPoolExecutor(max_workers=3)
        init_executor(self.executor)
        self.initialize()

    def stop(self):
        self.finished.set()

    def run(self):
        while not self.finished.is_set():
            self.finished.wait(self.interval)
            try:
                self.report_cash()
                self.report_positions()
                self.report_entrustments_changed()
            except Exception as e:
                logger.error(e)

    def initialize(self):
        self.cid_pool.setup()

    @trading_time
    def report_cash(self):
        balance = self.get_balance()
        if not balance:
            return

        acc_balance = balance[0]
        entrustments = self.get_entrustments()
        filtered_entrustments = filter(
            lambda x: x['state'] in [1, 3] and x['buy_or_sell'] == TradeCode.BUY, entrustments
        )
        acc_balance['used_cash'] = sum([e['price'] * e['quantity'] for e in filtered_entrustments])
        env.redis_client.hset(ACCOUNTS, self.account.account, json.dumps(acc_balance))
        env.redis_client.expire(ACCOUNTS, Datetime.remain_trade_seconds())

    @trading_time
    def report_positions(self):
        shares = self.get_shares()
        if not shares:
            return

        env.redis_client.hset(SHARES, self.account.account, json.dumps(shares, ensure_ascii=False))
        env.redis_client.expire(SHARES, Datetime.remain_trade_seconds())

    @staticmethod
    def get_changed_entrustments(entrustments):
        ret = []
        for e in entrustments:
            state = e['state']
            key = f'{e["account"]}_{e["security"]}_{e["hth"]}'
            if state and cache.get(key) != state:
                ret.append(e)
        return ret

    @trading_time
    def report_entrustments_changed(self):
        result = self.get_entrustments()
        changed_entrustments = self.get_changed_entrustments(result)
        if not changed_entrustments:
            return
        rsp = requests.post(env.hunter_url, json.dumps(changed_entrustments))
        if rsp.status_code == 200:
            for e in changed_entrustments:
                key = f'{self.account.account}_{e["security"]}_{e["hth"]}'
                cache.set(key, e['state'], 21600)

    @contextmanager
    def get_cid(self):
        cid = self.cid_pool.get()
        try:
            yield cid
        finally:
            if cid.state:
                self.cid_pool.put(cid)

    @cache.memoize(timeout=3)
    def _query(self, cid, category):
        parse = get_query_parse(category)
        try:
            return parse(self.account.account, query_data(cid.cid, category))
        except Exception as e:
            logger.error(e)
            cid.state = False

    def get_gddm(self, exchange):
        if exchange == 'XSHE':
            return self.account.gddm_sz
        else:
            return self.account.gddm_sh

    def _buy(self, cid, security, price, quantity):
        zqdm, exchange = security.split('.')
        gddm = self.get_gddm(exchange)
        try:
            ret = send_order(cid.cid,
                             SendOrderCategory.BUY,
                             SendOrderPriceType.LIMIT_CHARGE,
                             gddm,
                             zqdm,
                             price,
                             quantity)
            ret = parse_buy_or_sell(ret)
            ret['security'] = security
            ret['account'] = self.account.account
            ret['price'] = price
            ret['quantity'] = quantity
            return ret
        except Exception as e:
            logger.error(e)
            cid.state = False

    def _sell(self, cid, security, price, quantity):
        zqdm, exchange = security.split('.')
        gddm = self.get_gddm(exchange)
        try:
            ret = send_order(cid.cid,
                             SendOrderCategory.SELL,
                             SendOrderPriceType.LIMIT_CHARGE,
                             gddm,
                             zqdm,
                             price,
                             quantity)
            ret = parse_buy_or_sell(ret)
            ret['security'] = security
            ret['account'] = self.account.account
            ret['price'] = price
            ret['quantity'] = quantity
            return ret
        except Exception as e:
            logger.error(e)
            cid.state = False

    def _cancel(self, cid, security, hth):
        _, exchange = security.split('.')
        exchange_id = exchange == 'XSHG' and '1' or '0'
        try:
            ret = cancel_order(cid.cid, exchange_id, hth)
            ret = parse_cancel(ret)
            ret['security'] = security
            ret['account'] = self.account.account
            ret['hth'] = hth
            return ret
        except Exception as e:
            logger.error(e)
            cid.state = False

    @retry(times=1)
    def call(self, action, *args):
        with self.get_cid() as cid:
            return self.action_mapping[action](cid, *args)

    def order(self, buy_or_sell, security, price, quantity_list):
        if len(quantity_list) == 1:
            return [self.call(buy_or_sell, security, price, quantity_list[0])]
        else:
            futures = [self.executor.submit(self.call, buy_or_sell, security, price, q) for q in quantity_list]
            return [f.result() for f in as_completed(futures)]

    def cancel(self, security, hth_list):
        if len(hth_list) == 1:
            return [self.call(TradeCode.CANCEL, security, hth_list[0])]
        else:
            futures = [self.executor.submit(self.call, TradeCode.CANCEL, security, q) for q in hth_list]
            return [f.result() for f in as_completed(futures)]

    def get_new_stocks(self):
        return []

    def apply_new_stocks(self):
        pass

    def reverse_repo(self):
        pass

    def get_entrustments(self):
        return self.call(TradeCode.QUERY, QueryDataCategory.TODAY_ENTRUSTMENT)

    def get_shares(self):
        return self.call(TradeCode.QUERY, QueryDataCategory.SHARES)

    def get_balance(self):
        return self.call(TradeCode.QUERY, QueryDataCategory.BALANCE)
